1use crate::copp::InterpolationMode;
55use crate::copp::copp3::exact_interval::{
56 IntervalANonnegativeClassification, certify_interval_endpoint_ln,
57 classify_interval_a_nonnegative, exact_interval_negative_m_abs_ln,
58 exact_interval_nonnegative_m_ln, exact_interval_positive_d_ln,
59 profile_interval_uses_right_anchor,
60};
61use crate::copp::copp3::{Topp3ProfileMut, Topp3ProfileRef};
62use crate::diag::{
63 CoppError, check_input_len_at_least, check_input_len_equal, check_input_non_negative,
64 check_input_not_empty, check_input_not_nan_infinite, check_input_slice_non_negative,
65 check_input_slice_not_nan_infinite, check_input_strictly_increasing,
66};
67use crate::math::numerical::{EPS_ZERO, solve_2x2};
68use itertools::izip;
69use nalgebra::ComplexField;
70
71const TAYLOR_INVERSE_CONSTANT_JERK: f64 = 1E-3;
73const TAYLOR_INTERVAL_TIME_Z: f64 = 1E-4;
75const INTERVAL_TIME_NEAR_ONE_DELTA: f64 = 64.0 * f64::EPSILON;
78
79pub fn s_to_t_topp3(
103 s: &[f64],
104 profile: Topp3ProfileRef<'_>,
105 t0: f64,
106) -> Result<(f64, Vec<f64>), CoppError> {
107 check_topp3_sab("s_to_t_topp3", s, profile)?;
108 check_input_not_nan_infinite("s_to_t_topp3", "t0", t0)?;
109 let (a, b, num_stationary) = profile;
110 let mut t_s = Vec::<f64>::with_capacity(s.len()); let mut t_prev = t0;
112 let n = s.len() - 1;
113 t_s.push(t_prev);
114 if num_stationary.0 > 0 {
115 let s0 = s.first().unwrap();
116 t_s.resize(1 + num_stationary.0, t_prev);
117 for (t_curr, a_curr, s_curr) in izip!(t_s.iter_mut(), a.iter(), s.iter()).skip(1) {
118 *t_curr += 3.0 * (s_curr - s0) / a_curr.sqrt();
119 }
120 t_prev = *t_s.last().unwrap();
121 }
122 for (id, (s_pair, b_pair, a_pair)) in izip!(s.windows(2), b.windows(2), a.windows(2))
123 .enumerate()
124 .skip(num_stationary.0)
125 .take(n - num_stationary.0 - num_stationary.1)
126 {
127 t_prev += integral_profile_interval(
128 s_pair[1] - s_pair[0],
129 a_pair,
130 b_pair,
131 id + 1 == n - num_stationary.1,
132 );
133 t_s.push(t_prev);
134 }
135 if num_stationary.1 > 0 {
136 let s_final = s.last().unwrap();
137 let t_final =
138 t_prev + 3.0 * (s_final - s[n - num_stationary.1]) / a[n - num_stationary.1].sqrt();
139 t_s.resize(s.len(), t_final);
140 if num_stationary.1 > 1 {
141 for (t_curr, a_curr, s_curr) in izip!(t_s.iter_mut(), a.iter(), s.iter())
142 .rev()
143 .skip(1)
144 .take(num_stationary.1 - 1)
145 {
146 *t_curr += 3.0 * (s_curr - s_final) / a_curr.sqrt();
147 }
148 }
149 }
150
151 let t_final = *t_s.last().unwrap();
152 let all_finite = t_s.iter().fold(true, |acc, value| acc & value.is_finite());
156 if !all_finite {
157 let index = t_s
164 .iter()
165 .position(|value| !value.is_finite())
166 .unwrap_or(t_s.len() - 1);
167 let lo = index.saturating_sub(1);
171 let hi = (index + 1).min(a.len() - 1);
172 let window = (lo..=hi)
173 .map(|j| {
174 format!(
175 " [{j}] a = {:.6e}, b = {:.6e}, s = {:.6e}",
176 a[j], b[j], s[j]
177 )
178 })
179 .collect::<Vec<_>>()
180 .join("\n");
181 return Err(CoppError::InvalidInput(
182 "s_to_t_topp3".into(),
183 format!(
184 "computed time profile contains NaN or infinity: first at index {index} \
185 (t = {}), num_stationary = {num_stationary:?}\n{window}",
186 t_s[index]
187 ),
188 ));
189 }
190 check_input_strictly_increasing("s_to_t_topp3", "t_s", &t_s)?;
191 Ok((t_final, t_s))
192}
193
194pub fn t_to_s_topp3(
220 s: &[f64],
221 profile: Topp3ProfileRef<'_>,
222 t_s: &[f64],
223 mode: InterpolationMode<'_>,
224) -> Result<Vec<f64>, CoppError> {
225 check_topp3_sab("t_to_s_topp3", s, profile)?;
226 check_input_len_equal(
227 "t_to_s_topp3",
228 "`t_s.len()`",
229 t_s.len(),
230 "`s.len()`",
231 s.len(),
232 )?;
233 check_input_slice_not_nan_infinite("t_to_s_topp3", "t_s", t_s)?;
234 check_input_strictly_increasing("t_to_s_topp3", "t_s", t_s)?;
235 match mode {
236 InterpolationMode::UniformTimeGrid(t0, dt, include_final) => {
237 check_input_not_nan_infinite("t_to_s_topp3", "t0", t0)?;
238 check_input_not_nan_infinite("t_to_s_topp3", "dt", dt)?;
239 if dt <= 0.0 {
240 return Err(CoppError::InvalidInput(
241 "t_to_s_topp3".into(),
242 format!("`dt` = {dt} must be positive"),
243 ));
244 }
245 let num_t = ((t_s.last().unwrap() - t0) / dt).floor() as usize;
247 let mut s_t = t_to_s_topp3_core(
248 s,
249 profile,
250 t_s,
251 (0..num_t).map(|i| t0 + i as f64 * dt),
252 num_t,
253 );
254 if include_final {
255 let flag = if s_t.is_empty() {
256 t0 <= *t_s.last().unwrap()
257 } else {
258 *s_t.last().unwrap() < *s.last().unwrap()
259 };
260 if flag {
261 s_t.push(*s.last().unwrap());
262 }
263 }
264 Ok(s_t)
265 }
266 InterpolationMode::NonUniformTimeGrid(t_sample) => {
267 check_input_not_empty("t_to_s_topp3", "`t_sample`", t_sample.len())?;
268 check_input_slice_not_nan_infinite("t_to_s_topp3", "t_sample", t_sample)?;
269 check_input_strictly_increasing("t_to_s_topp3", "t_sample", t_sample)?;
270 Ok(t_to_s_topp3_core(
271 s,
272 profile,
273 t_s,
274 t_sample.iter().cloned(),
275 t_sample.len(),
276 ))
277 }
278 }
279}
280
281fn t_to_s_topp3_core(
287 s: &[f64],
288 profile: Topp3ProfileRef<'_>,
289 t_s: &[f64],
290 mut t_sample: impl Iterator<Item = f64>,
291 len_t_sample: usize,
292) -> Vec<f64> {
293 let (a, b, num_stationary) = profile;
294 let &t_start = t_s.first().unwrap();
296 let &t_final = t_s.last().unwrap();
297 let mut s_t = Vec::<f64>::with_capacity(len_t_sample + 1); let Some(mut t_curr) = t_sample.next() else {
299 return vec![];
300 };
301 while t_curr < t_start {
302 s_t.push(f64::NAN);
303 let Some(t) = t_sample.next() else {
304 return s_t;
305 };
306 t_curr = t;
307 }
308
309 if num_stationary.0 > 0 {
310 let s0 = s.first().unwrap();
311 let a_stationary = a[num_stationary.0];
312 let t_stationary = t_s[num_stationary.0];
313 let d3u_over_6 =
314 a_stationary.sqrt() * a_stationary / (27.0 * (s[num_stationary.0] - s0).powi(2));
315 while t_curr <= t_stationary {
316 s_t.push(s0 + d3u_over_6 * (t_curr - t_start).powi(3));
317 let Some(t) = t_sample.next() else {
318 return s_t;
319 };
320 t_curr = t;
321 }
322 }
323
324 for (id, (s_pair, a_pair, b_pair, t_pair)) in
325 izip!(s.windows(2), a.windows(2), b.windows(2), t_s.windows(2))
326 .enumerate()
327 .skip(num_stationary.0)
328 .take(s.len() - num_stationary.0 - num_stationary.1 - 1)
329 {
330 let h = s_pair[1] - s_pair[0];
331 let reverse = profile_interval_uses_right_anchor(
332 a_pair[0],
333 a_pair[1],
334 id + 1 == s.len() - num_stationary.1 - 1,
335 );
336 while t_curr <= t_pair[1] {
337 let position = if t_curr == t_pair[1] {
340 s_pair[1]
341 } else if t_curr == t_pair[0] {
342 s_pair[0]
343 } else if reverse {
344 s_pair[1]
345 - inverse_constant_jerk_interval(
346 h,
347 a_pair[1],
348 -b_pair[1],
349 -b_pair[0],
350 t_pair[1] - t_curr,
351 )
352 } else {
353 s_pair[0]
354 + inverse_constant_jerk_interval(
355 h,
356 a_pair[0],
357 b_pair[0],
358 b_pair[1],
359 t_curr - t_pair[0],
360 )
361 };
362 s_t.push(position);
363 let Some(t) = t_sample.next() else {
364 return s_t;
365 };
366 t_curr = t;
367 }
368 }
369
370 if num_stationary.1 > 0 {
371 let s_final = s.last().unwrap();
372 let a_stationary = a[s.len() - num_stationary.1 - 1];
373 let d3u_over_6 = a_stationary.sqrt() * a_stationary
374 / (27.0 * (s_final - s[s.len() - num_stationary.1 - 1]).powi(2));
375 while t_curr <= t_final {
376 s_t.push(s_final + d3u_over_6 * (t_curr - t_final).powi(3));
377 let Some(t) = t_sample.next() else {
378 return s_t;
379 };
380 t_curr = t;
381 }
382 }
383
384 s_t.push(f64::NAN);
385 while t_sample.next().is_some() {
386 s_t.push(f64::NAN);
387 }
388 s_t
389}
390
391#[inline]
395fn integral_profile_interval(h: f64, a: &[f64], b: &[f64], last_ordinary: bool) -> f64 {
396 profile_interval_time(h, a[0], b[0], a[1], b[1], last_ordinary)
397}
398
399#[inline]
403pub(crate) fn profile_interval_time(
404 h: f64,
405 a_left: f64,
406 b_left: f64,
407 a_right: f64,
408 b_right: f64,
409 last_ordinary: bool,
410) -> f64 {
411 if (a_left == 0.0 && b_left == 0.0) || (a_right == 0.0 && b_right == 0.0) {
412 return f64::INFINITY;
413 }
414 if profile_interval_uses_right_anchor(a_left, a_right, last_ordinary) {
415 integral_constant_jerk_interval(h, a_right, -b_right, -b_left)
416 } else {
417 integral_constant_jerk_interval(h, a_left, b_left, b_right)
418 }
419}
420
421#[inline(always)]
424fn interval_time_psi(z: f64) -> f64 {
425 if z.abs() < TAYLOR_INTERVAL_TIME_Z {
426 z.mul_add(z.mul_add(z.mul_add(1.0 / 7.0, 1.0 / 5.0), 1.0 / 3.0), 1.0)
427 } else {
428 let root = z.abs().sqrt();
429 if z > 0.0 {
430 root.atanh() / root
431 } else {
432 root.atan() / root
433 }
434 }
435}
436
437#[inline(always)]
438fn logaddexp(lhs: f64, rhs: f64) -> f64 {
439 if lhs == f64::INFINITY || rhs == f64::INFINITY {
440 return f64::INFINITY;
441 }
442 if lhs == f64::NEG_INFINITY {
443 return rhs;
444 }
445 if rhs == f64::NEG_INFINITY {
446 return lhs;
447 }
448 let high = lhs.max(rhs);
449 let low = lhs.min(rhs);
450 high + (low - high).exp().ln_1p()
451}
452
453#[inline(always)]
454fn interval_time_scaled_z(h: f64, b: f64, u: f64, sqrt_sum: f64) -> (f64, f64) {
455 if u == b {
456 return (0.0, f64::NEG_INFINITY);
457 }
458 let difference = u - b;
459 let ln_abs_difference = if difference.is_finite() {
460 difference.abs().ln()
461 } else {
462 logaddexp(u.abs().ln(), b.abs().ln())
463 };
464 let ln_magnitude = h.ln() + ln_abs_difference - 2.0 * sqrt_sum.ln();
465 let magnitude = ln_magnitude.exp();
466 (if u > b { magnitude } else { -magnitude }, ln_magnitude)
467}
468
469#[inline(always)]
470fn interval_time_scaled_value(h: f64, sqrt_sum: f64, d: f64, psi: f64) -> f64 {
471 if d.is_finite() && d > 0.0 {
472 let direct = d * (2.0 * psi);
473 if direct.is_finite() && direct > 0.0 {
474 return direct;
475 }
476 }
477 (std::f64::consts::LN_2 + h.ln() - sqrt_sum.ln() + psi.ln()).exp()
478}
479
480#[inline(always)]
481fn interval_time_psi_near_one(ln_delta: f64) -> f64 {
482 debug_assert!(ln_delta < 0.0);
483 let z = (-ln_delta.exp_m1()).clamp(0.0, 1.0);
484 let root = z.sqrt();
485 (root.ln_1p() - 0.5 * ln_delta) / root
486}
487
488#[inline]
497fn integral_constant_jerk_interval(h: f64, a: f64, b: f64, u: f64) -> f64 {
498 let Some(classified) = classify_interval_a_nonnegative(h, a, b, u) else {
499 return f64::NAN;
500 };
501 let m = h.mul_add(b, a);
502 let a_next = h.mul_add(u, m);
503 let exact_endpoint_ln = match certify_interval_endpoint_ln(h, a, b, u, m, a_next) {
504 Ok(value) => value,
505 Err(()) => return f64::NAN,
506 };
507 let sqrt_a = a.sqrt();
508 let sqrt_next = match exact_endpoint_ln {
509 Some(ln_endpoint) if ln_endpoint.is_finite() => (0.5 * ln_endpoint).exp(),
510 Some(_) => 0.0,
511 None => a_next.sqrt(),
512 };
513 let curvature = (u - b) / h;
514
515 if (a == 0.0 && b > 0.0 && u == -b) || (a == 0.0 && sqrt_next == 0.0 && curvature < 0.0) {
517 return std::f64::consts::PI / (-curvature).sqrt();
518 }
519 if (a == 0.0 && b == 0.0 && u >= 0.0) || (sqrt_next == 0.0 && u == 0.0 && b <= 0.0) {
520 return f64::INFINITY;
521 }
522 if matches!(classified, IntervalANonnegativeClassification::InteriorZero) {
523 return f64::INFINITY;
524 }
525
526 let sqrt_sum = sqrt_a + sqrt_next;
527 let d = h / sqrt_sum;
528 let d2 = d * d;
529 let raw_z = curvature * d2;
530 let unsafe_scale =
531 d == 0.0 || !d.is_finite() || d2 == 0.0 || !d2.is_finite() || !raw_z.is_finite();
532 let (z, ln_abs_z) = if unsafe_scale {
533 interval_time_scaled_z(h, b, u, sqrt_sum)
534 } else {
535 (raw_z, raw_z.abs().ln())
536 };
537 let near_singularity = if unsafe_scale {
538 z > 0.5
539 } else {
540 z >= 1.0 - INTERVAL_TIME_NEAR_ONE_DELTA
541 };
542
543 if near_singularity {
544 if !d.is_finite() {
545 return f64::INFINITY;
546 }
547 let ln_delta = match classified {
548 IntervalANonnegativeClassification::InteriorPositive => {
549 let Some(ln_d) = exact_interval_positive_d_ln(h, a, b, u) else {
550 return f64::NAN;
551 };
552 let Some(ln_abs_m) = exact_interval_negative_m_abs_ln(h, a, b) else {
553 return f64::NAN;
554 };
555 let ln_pq = sqrt_a.ln() + sqrt_next.ln();
556 std::f64::consts::LN_2 + h.ln() + ln_d
557 - logaddexp(ln_pq, ln_abs_m)
558 - 2.0 * sqrt_sum.ln()
559 }
560 IntervalANonnegativeClassification::Endpoint => {
561 let Some(ln_m) = exact_interval_nonnegative_m_ln(h, a, b) else {
562 return f64::NAN;
563 };
564 let ln_pq = if sqrt_a == 0.0 || sqrt_next == 0.0 {
565 f64::NEG_INFINITY
566 } else {
567 sqrt_a.ln() + sqrt_next.ln()
568 };
569 std::f64::consts::LN_2 + logaddexp(ln_m, ln_pq) - 2.0 * sqrt_sum.ln()
570 }
571 IntervalANonnegativeClassification::InteriorZero => return f64::INFINITY,
572 };
573 if ln_delta.is_finite() && ln_delta < 0.0 {
574 let psi = interval_time_psi_near_one(ln_delta);
575 return interval_time_scaled_value(h, sqrt_sum, d, psi);
576 }
577 if !z.is_finite() || z >= 1.0 {
578 return f64::INFINITY;
579 }
580 }
581
582 if unsafe_scale && z.is_sign_negative() && ln_abs_z > 0.5 * f64::MAX.ln() {
583 let ln_psi = std::f64::consts::FRAC_PI_2.ln() - 0.5 * ln_abs_z;
584 return (std::f64::consts::LN_2 + h.ln() - sqrt_sum.ln() + ln_psi).exp();
585 }
586
587 let psi = interval_time_psi(z);
588 if unsafe_scale || z > 0.5 {
589 interval_time_scaled_value(h, sqrt_sum, d, psi)
590 } else {
591 2.0 * d * psi
592 }
593}
594
595#[inline(always)]
600fn inverse_constant_jerk_interval(h: f64, a: f64, b: f64, u: f64, dt: f64) -> f64 {
601 if dt == 0.0 {
602 return 0.0;
603 }
604 let eps = ((u - b) / h) * dt * dt;
605 let sqrt_a = a.sqrt();
606 let half_b_dt = 0.5 * b * dt;
607
608 if eps.abs() < TAYLOR_INVERSE_CONSTANT_JERK {
609 let first = eps.mul_add(
610 eps.mul_add(eps.mul_add(1.0 / 5040.0, 1.0 / 120.0), 1.0 / 6.0),
611 1.0,
612 );
613 let second = eps.mul_add(
614 eps.mul_add(eps.mul_add(1.0 / 20160.0, 1.0 / 360.0), 1.0 / 12.0),
615 1.0,
616 );
617 dt * sqrt_a.mul_add(first, half_b_dt * second)
618 } else if eps > 0.0 {
619 let k = eps.sqrt();
620 let sinh_half_over_k = (0.5 * k).sinh() / k;
621 dt * sqrt_a.mul_add(
622 k.sinh() / k,
623 2.0 * b * dt * sinh_half_over_k * sinh_half_over_k,
624 )
625 } else {
626 let k = (-eps).sqrt();
627 let sin_half_over_k = (0.5 * k).sin() / k;
628 dt * sqrt_a.mul_add(
629 k.sin() / k,
630 2.0 * b * dt * sin_half_over_k * sin_half_over_k,
631 )
632 }
633}
634
635pub fn force_positive_a(
651 profile: Topp3ProfileMut<'_>,
652 s: &[f64],
653 a_min: f64,
654) -> Result<bool, CoppError> {
655 let (a, b, num_stationary) = profile;
656 let n = s.len();
657 if a.len() != n || b.len() != n {
658 return Err(CoppError::InvalidInput(
659 "force_positive_a".into(),
660 format!(
661 "`a.len()` = {} and `b.len()` = {} must equal `s.len()` = {}",
662 a.len(),
663 b.len(),
664 n
665 ),
666 ));
667 }
668 if n < 4 {
669 return Err(CoppError::InvalidInput(
670 "force_positive_a".into(),
671 format!("`s.len()` = {n} must be at least 4"),
672 ));
673 }
674 check_stationary_counts("force_positive_a", n, num_stationary)?;
675 check_input_slice_not_nan_infinite("force_positive_a", "s", s)?;
676 check_input_slice_non_negative("force_positive_a", "a", a)?;
677 check_input_slice_not_nan_infinite("force_positive_a", "b", b)?;
678 check_input_non_negative("force_positive_a", "a_min", a_min)?;
679 check_input_strictly_increasing("force_positive_a", "s", s)?;
680 let mut flag_succeed = true;
682 for i in (num_stationary.0 + 1)..(n - 2 - num_stationary.1) {
683 let b1 = b[i];
685 let b2 = b[i + 1];
686 if b1 < 0.0 && b2 > 0.0 {
687 let s1 = s[i];
691 let s2 = s[i + 1];
692 let ds1 = s2 - s1;
693 let a1 = a[i];
694 let amin = a_min.max(a1.min(a[i + 1]));
695 let amin = if amin > 10.0 * EPS_ZERO {
696 0.1 * amin
697 } else if amin > EPS_ZERO {
698 EPS_ZERO
699 } else {
700 amin
701 };
702 let da = a1 - amin;
703 let db = b2 - b1;
704 if b1 * b1 * ds1 >= da * db {
705 let s0 = s[i - 1];
711 let s3 = s[i + 2];
712 let delta_s_end = (s3 - s0, s3 - s1, s3 - s2);
713 let coeff = match solve_2x2(
714 (
715 (delta_s_end.1, delta_s_end.2),
716 (delta_s_end.1 * delta_s_end.1, delta_s_end.2 * delta_s_end.2),
717 ),
718 (-delta_s_end.0, -delta_s_end.0 * delta_s_end.0),
719 ) {
720 Some(coeff) => {
721 coeff
723 }
724 None => {
725 crate::verbosity_log!(
726 crate::diag::Verbosity::Debug,
727 "coeff is None? A = {:?}, b = {:?}",
728 (
729 (delta_s_end.1, delta_s_end.2),
730 (delta_s_end.1 * delta_s_end.1, delta_s_end.2 * delta_s_end.2)
731 ),
732 (-delta_s_end.0, -delta_s_end.0 * delta_s_end.0)
733 );
734 flag_succeed = false;
735 continue;
736 }
737 };
738 let ds0 = s1 - s0;
741 let coeff_c = (ds0 * ds0, ds0, ds0 + ds1 * (1.0 + coeff.0));
742 let coeff_solve = (
749 coeff_c.1 * coeff_c.1 * ds1 - coeff_c.0 * (coeff_c.2 - coeff_c.1),
750 2.0 * b1 * coeff_c.1 * ds1 - da * (coeff_c.2 - coeff_c.1) - coeff_c.0 * db,
751 b1 * b1 * ds1 - da * db,
752 );
753 let norm = coeff_solve.0.abs() + coeff_solve.1.abs() + coeff_solve.2.abs();
754 if norm < EPS_ZERO {
755 crate::verbosity_log!(
756 crate::diag::Verbosity::Debug,
757 "norm = {norm} < EPS_ZERO, coeff_solve = {coeff_solve:.8?}"
758 );
759 flag_succeed = false;
760 continue;
761 }
762 let norm_inv = 1.0 / norm;
763 let coeff_solve = (
764 coeff_solve.0 * norm_inv,
765 coeff_solve.1 * norm_inv,
766 coeff_solve.2 * norm_inv,
767 );
768 let c0 = if coeff_solve.0.abs() > EPS_ZERO {
770 let discriminant =
772 coeff_solve.1 * coeff_solve.1 - 4.0 * coeff_solve.0 * coeff_solve.2;
773 if discriminant < 0.0 {
774 if coeff_c.1.abs() > EPS_ZERO && coeff_c.2.abs() > EPS_ZERO {
775 (-b1 / coeff_c.1).min(b2 / coeff_c.2)
776 } else if coeff_c.1.abs() > EPS_ZERO {
777 -b1 / coeff_c.1
778 } else if coeff_c.2.abs() > EPS_ZERO {
779 b2 / coeff_c.2
780 } else {
781 crate::verbosity_log!(
782 crate::diag::Verbosity::Debug,
783 "discriminant = {discriminant:.8} < 0 for c0 (i={i}): coeff_solve = {coeff_solve:.8?}, coeff_c = {coeff_c:.8?}"
784 );
785 flag_succeed = false;
786 continue;
787 }
788 } else {
789 let sqrt_discriminant = discriminant.sqrt();
790 let c0 = if coeff_solve.0 > 0.0 {
792 (
793 (-coeff_solve.1 + sqrt_discriminant) / (2.0 * coeff_solve.0),
794 (-coeff_solve.1 - sqrt_discriminant) / (2.0 * coeff_solve.0),
795 )
796 } else {
797 (
798 (-coeff_solve.1 - sqrt_discriminant) / (2.0 * coeff_solve.0),
799 (-coeff_solve.1 + sqrt_discriminant) / (2.0 * coeff_solve.0),
800 )
801 };
802 if c0.1 >= 0.0 { c0.1 } else { c0.0 }
803 }
804 } else {
805 -coeff_solve.2 / coeff_solve.1
807 };
808 a[i] += coeff_c.0 * c0;
809 b[i] += coeff_c.1 * c0;
810 b[i + 1] += coeff_c.2 * c0;
811 a[i + 1] += (coeff_c.0 + (coeff_c.1 + coeff_c.2) * ds1) * c0;
812 }
813 }
814 }
815
816 Ok(flag_succeed)
817}
818
819fn check_topp3_sab(
825 function_name: &str,
826 s: &[f64],
827 profile: Topp3ProfileRef<'_>,
828) -> Result<(), CoppError> {
829 let (a, b, num_stationary) = profile;
830 check_stationary_counts(function_name, s.len(), num_stationary)?;
831 if a.len() != s.len() || b.len() != s.len() {
832 return Err(CoppError::InvalidInput(
833 function_name.into(),
834 format!(
835 "`a.len()` = {} and `b.len()` = {} must equal `s.len()` = {}",
836 a.len(),
837 b.len(),
838 s.len()
839 ),
840 ));
841 }
842 check_input_slice_not_nan_infinite(function_name, "s", s)?;
843 check_input_slice_non_negative(function_name, "a", a)?;
844 check_input_slice_not_nan_infinite(function_name, "b", b)?;
845 check_input_strictly_increasing(function_name, "s", s)
846}
847
848fn check_stationary_counts(
854 function_name: &str,
855 s_len: usize,
856 num_stationary: (usize, usize),
857) -> Result<(), CoppError> {
858 let Some(min_len) = num_stationary
859 .0
860 .checked_add(num_stationary.1)
861 .and_then(|sum| sum.checked_add(2))
862 else {
863 return Err(CoppError::InvalidInput(
864 function_name.into(),
865 "`num_stationary` overflowed while checking dimensions".into(),
866 ));
867 };
868 check_input_len_at_least(function_name, "`s.len()`", s_len, min_len)
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874
875 fn assert_close(actual: f64, expected: f64, relative: f64) {
876 let scale = actual.abs().max(expected.abs()).max(1.0);
877 assert!(
878 (actual - expected).abs() <= relative * scale,
879 "actual={actual:.17e}, expected={expected:.17e}"
880 );
881 }
882
883 #[test]
884 fn profile_time_reverse_fixed_zero_round_trip() -> Result<(), CoppError> {
885 let s = [0.0_f64, 1.0];
886 let b = [-0.1_f64, -0.4];
887 let a = [(-1.0_f64).mul_add(b[0] + b[1], 0.0), 0.0];
888 assert!(integral_constant_jerk_interval(1.0, a[0], b[0], b[1]).is_nan());
889 let (total, knots) = s_to_t_topp3(&s, (&a, &b, (0, 0)), 0.0)?;
890 assert!(total.is_finite() && total > 0.0);
891 let queries = [0.0, 0.25 * total, 0.5 * total, 0.75 * total, total];
892 let positions = t_to_s_topp3(
893 &s,
894 (&a, &b, (0, 0)),
895 &knots,
896 InterpolationMode::NonUniformTimeGrid(&queries),
897 )?;
898 assert_eq!(positions[0], s[0]);
899 assert_eq!(positions[4], s[1]);
900 assert!(positions.windows(2).all(|pair| pair[0] < pair[1]));
901 for (&time, &position) in queries[1..4].iter().zip(&positions[1..4]) {
902 let distance = 1.0 - position;
903 let reverse_b = -b[1] + (b[1] - b[0]) * distance;
904 let remaining = integral_constant_jerk_interval(distance, 0.0, -b[1], reverse_b);
905 assert_close(remaining, total - time, 2.0e-13);
906 }
907 Ok(())
908 }
909
910 #[test]
911 fn profile_time_preserves_stationary_divergence_and_simple_zeros() {
912 let s = [0.0, 1.0];
913 for (a, b) in [([0.0, 1.0], [0.0, 1.0]), ([1.0, 0.0], [-1.0, 0.0])] {
914 assert!(s_to_t_topp3(&s, (&a, &b, (0, 0)), 0.0).is_err());
915 }
916 let (total, _) = s_to_t_topp3(&[0.0, 2.0], (&[0.0, 0.0], &[1.0, -1.0], (0, 0)), 0.0)
917 .expect("two simple zeros have finite time");
918 assert_close(total, std::f64::consts::PI, 4.0 * f64::EPSILON);
919 }
920
921 #[test]
922 fn profile_anchor_does_not_retry_the_easier_soc_direction() {
923 assert!(integral_constant_jerk_interval(1.0, 1.0, -1.0, 1.0).is_finite());
926 assert!(s_to_t_topp3(&[0.0, 1.0], (&[1.0, 0.25], &[-1.0, 1.0], (0, 0)), 0.0).is_err());
927 }
928
929 #[test]
930 fn natural_interval_time_handles_zero_curvature_and_two_simple_zeros() {
931 assert_close(
932 integral_constant_jerk_interval(3.0, 4.0, 0.0, 0.0),
933 1.5,
934 4.0 * f64::EPSILON,
935 );
936 assert_close(
937 integral_constant_jerk_interval(2.0, 0.0, 1.0, -1.0),
938 std::f64::consts::PI,
939 4.0 * f64::EPSILON,
940 );
941 }
942
943 #[test]
944 fn natural_interval_time_does_not_turn_an_exact_double_root_into_a_finite_time() {
945 let (h, a, b, u) = (6.0, 3.0, -1.0, 1.0);
950 assert!(matches!(
951 classify_interval_a_nonnegative(h, a, b, u),
952 Some(IntervalANonnegativeClassification::InteriorZero)
953 ));
954 assert_eq!(integral_constant_jerk_interval(h, a, b, u), f64::INFINITY);
955 assert!(integral_constant_jerk_interval(h, a, b, u.next_up()).is_finite());
956 assert!(integral_constant_jerk_interval(h, a, b, u.next_down()).is_nan());
957
958 let s = [0.0, h];
959 let acceleration = [a, h.mul_add(u, h.mul_add(b, a))];
960 let slope = [b, u];
961 assert!(s_to_t_topp3(&s, (&acceleration, &slope, (0, 0)), 0.0).is_err());
962 }
963
964 #[test]
965 fn natural_time_and_inverse_round_trip_for_all_curvature_signs() -> Result<(), CoppError> {
966 let cases = [
967 (0.5, 1.0, -0.1, 0.4),
968 (0.5, 1.0, 0.2, 0.2),
969 (0.5, 1.0, 0.5, -0.2),
970 ];
971 for (h, a, b, u) in cases {
972 let a_next = h.mul_add(u, h.mul_add(b, a));
973 let s = [0.0, h];
974 let acceleration = [a, a_next];
975 let slope = [b, u];
976 let (t_final, t_s) = s_to_t_topp3(&s, (&acceleration, &slope, (0, 0)), 0.0)?;
977 assert!(t_final.is_finite() && t_final > 0.0);
978
979 let query = [0.0, 0.25 * t_final, 0.5 * t_final, 0.75 * t_final, t_final];
980 let sampled = t_to_s_topp3(
981 &s,
982 (&acceleration, &slope, (0, 0)),
983 &t_s,
984 InterpolationMode::NonUniformTimeGrid(&query),
985 )?;
986 assert_eq!(sampled.len(), query.len());
987 assert_eq!(sampled[0], 0.0);
988 assert_eq!(sampled[query.len() - 1], h);
989 for pair in sampled.windows(2) {
990 assert!(pair[0] < pair[1], "non-monotone inverse: {sampled:?}");
991 }
992
993 for (&time, &x) in query[1..query.len() - 1]
994 .iter()
995 .zip(&sampled[1..sampled.len() - 1])
996 {
997 let b_at_x = ((u - b) / h).mul_add(x, b);
998 let reconstructed = integral_constant_jerk_interval(x, a, b, b_at_x);
999 assert_close(reconstructed, time, 2.0e-13);
1000 }
1001 }
1002 Ok(())
1003 }
1004
1005 #[test]
1006 fn stationary_head_and_tail_convention_is_unchanged() -> Result<(), CoppError> {
1007 let s = [0.0, 1.0, 2.0, 3.0];
1008 let a = [0.0, 1.0, 1.0, 0.0];
1009 let b = [0.0, 0.0, 0.0, 0.0];
1010 let (t_final, t_s) = s_to_t_topp3(&s, (&a, &b, (1, 1)), 0.0)?;
1011 assert_eq!(t_final, 7.0);
1012 assert_eq!(t_s, [0.0, 3.0, 4.0, 7.0]);
1013
1014 let query = [0.0, 3.0, 3.5, 4.0, 7.0];
1015 let sampled = t_to_s_topp3(
1016 &s,
1017 (&a, &b, (1, 1)),
1018 &t_s,
1019 InterpolationMode::NonUniformTimeGrid(&query),
1020 )?;
1021 assert_eq!(sampled, [0.0, 1.0, 1.5, 2.0, 3.0]);
1022 Ok(())
1023 }
1024}